import { rm, mkdir, writeFile, symlink } from 'node:fs/promises';
import { basename, resolve, extname } from 'node:path';
import { spawn } from 'node:child_process';
import process from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { getTmpDir, styleText } from '../common.js';
import { transpile } from './transpile.js';

const DEFAULT_SERVE_HOST = 'localhost';

export async function run(componentPath, args, opts) {
    // Ensure that `args` is an array
    args = [...args];
    return runComponent(
        componentPath,
        args,
        opts,
        `
    if (!mod.run || !mod.run.run) {
      console.error('Not a valid command component to execute.');
      process.exit(1);
    }
    try {
      mod.run.run();
      // for stdout flushing
      await new Promise(resolve => setTimeout(resolve));
      process.exit(0);
    }
    catch (e) {
      console.error(e);
      process.exit(1);
    }
  `
    );
}

export async function serve(componentPath, args, opts) {
    let tryFindPort = false;
    let { port, host } = opts;
    if (port === undefined) {
        tryFindPort = true;
        port = '8000';
    }
    // Ensure that `args` is an array
    args = [...args];
    host = host ?? DEFAULT_SERVE_HOST;
    return runComponent(
        componentPath,
        args,
        opts,
        `
    import { HTTPServer } from '@bytecodealliance/preview2-shim/http';
    const server = new HTTPServer(mod.incomingHandler);
    let port = ${port};
    ${
    tryFindPort
        ? `
    while (true) {
      try {
        server.listen(port, ${JSON.stringify(host)});
        break;
      } catch (e) {
        if (e.code !== 'EADDRINUSE')
          throw e;
      }
      port++;
    }
    `
        : `server.listen(port, ${JSON.stringify(host)})`
}
    console.error(\`Server listening @ ${host}:${port}...\`);
  `
    );
}

async function runComponent(componentPath, args, opts, executor) {
    const jcoImport = opts.jcoImport ? resolve(opts.jcoImport) : null;

    const name = basename(
        componentPath.slice(0, -extname(componentPath).length || Infinity)
    );
    const outDir = opts.jcoDir || (await getTmpDir());
    if (opts.jcoDir) {
        await mkdir(outDir, { recursive: true });
    }

    try {
        try {
            await transpile(componentPath, {
                name,
                quiet: true,
                noTypescript: true,
                wasiShim: true,
                outDir,
                tracing: opts.jcoTrace,
                map: opts.jcoMap,
                importBindings: opts.jcoImportBindings,
            });
        } catch (e) {
            throw new Error('Unable to transpile command for execution', {
                cause: e,
            });
        }

        await writeFile(
            resolve(outDir, 'package.json'),
            JSON.stringify({ type: 'module' })
        );

        let preview2ShimPath;
        try {
            preview2ShimPath = resolve(
                fileURLToPath(
                    import.meta.resolve('@bytecodealliance/preview2-shim')
                ),
                '../../../'
            );
        } catch (err) {
            let msg = `${styleText(['red', 'bold'], 'error')} Failed to resolve ${styleText('bold', '@bytecodealliance/preview2-shim')}, ensure it is installed.`;
            msg += `\nERROR:\n${err.toString()}`;
            throw new Error(msg);
        }

        const modulesDir = resolve(outDir, 'node_modules', '@bytecodealliance');
        await mkdir(modulesDir, { recursive: true });

        try {
            await symlink(
                preview2ShimPath,
                resolve(modulesDir, 'preview2-shim'),
                'dir'
            );
        } catch (e) {
            if (e.code !== 'EEXIST') {
                throw e;
            }
        }

        const runPath = resolve(outDir, '_run.js');
        await writeFile(
            runPath,
            `
      ${jcoImport ? `import ${JSON.stringify(pathToFileURL(jcoImport))}` : ''}
      import process from 'node:process';
      try {
        process.argv[1] = "${name}";
      } catch {}
      const mod = await import('./${name}.js');
      ${executor}
    `
        );

        const nodePath = process.env.JCO_RUN_PATH || process.argv[0];

        process.exitCode = await new Promise((resolve, reject) => {
            const cp = spawn(
                nodePath,
                [
                    ...(process.env.JCO_RUN_ARGS
                        ? process.env.JCO_RUN_ARGS.split(' ')
                        : []),
                    runPath,
                    ...args,
                ],
                { stdio: 'inherit' }
            );

            cp.on('error', reject);
            cp.on('exit', resolve);
        });
    } finally {
        try {
            if (!opts.jcoDir) {
                await rm(outDir, { recursive: true });
            }
        } catch {
            // empty
        }
    }
}
